#include "SqliteFileManager.h"
#include <sqlpp11/sqlpp11.h>
#include <sqlpp11/odbc/odbc.h>
#include <sqlpp11/sqlite3/sqlite3.h>
#include "sqlpp11/sqlpp11orm.h"

#include "OCMain/Environment.h"
#include "OCMain/FileUtil.h"
#include "OCMain/Log.h"
#include "drogon/drogon.h"

namespace sql = sqlpp::sqlite3;

namespace OCPP
{
	std::shared_ptr<sql::connection> openDB(String fileName, int flags)
	{
		sql::connection_config config;
		config.path_to_database = fileName;
		config.flags = flags;
		config.debug = false;

		try {
			return std::make_shared<sql::connection>(config);
		}
		catch (...) {
		}

		return nullptr;
	}

	std::shared_ptr<sql::connection> createDB(String fileName)
	{
		if (FileUtil::existFile(fileName))
		{
			return openDB(fileName, SQLITE_OPEN_READWRITE);
		}

		auto saveDB = openDB(fileName, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
		String sql1 = "CREATE TABLE IF NOT EXISTS file_info(\
				file_gid INTEGER PRIMARY KEY,\
				file_path TEXT,\
				file_md5 TEXT)";
		String sql2 = "CREATE TABLE IF NOT EXISTS file_data(\
				file_md5 TEXT PRIMARY KEY,\
				file_size INTEGER,\
				file_data BLOB)";
		saveDB->execute(sql1);
		saveDB->execute(sql2);
		saveDB->execute("CREATE UNIQUE INDEX file_path_index ON file_info(file_path);");
		saveDB->execute("CREATE UNIQUE INDEX file_md5_index ON file_data(file_md5);");
		return saveDB;
	}

	bool SqliteFileManager::existInfo(String path)
	{
		sql::connection& db = *connection;
		Tab_FDB::table_file_info table = {};
		auto result = (db)(select(count(table.file_path)).from(table).where(table.file_path == path));
		auto count = result.front().count;
		return count > 0;
	}

	bool SqliteFileManager::existData(String md5)
	{
		sql::connection& db = *connection;
		Tab_FDB::table_file_data table = {};
		auto result = (db)(select(count(table.file_md5)).from(table).where(table.file_md5 == md5));
		auto count = result.front().count;
		return count > 0;
	}

	std::tuple<std::set<String>, std::set<String>> SqliteFileManager::getExist()
	{
		sql::connection& db = *connection;
		std::set<String> path;
		std::set<String> md5;

		{
			Tab_FDB::table_file_info table = {};
			auto result = (db)(select(table.file_path).from(table).unconditionally());
			for (const auto& row : result)
			{
				path.insert(row.file_path);
			}
		}

		{
			Tab_FDB::table_file_data table = {};
			auto result = (db)(select(table.file_md5).from(table).unconditionally());
			for (const auto& row : result)
			{
				md5.insert(row.file_md5);
			}
		}
		return std::make_tuple<>(path, md5);
	}

	StringVector findFiles(String path, bool sort = false)
	{
		StringVector vec = FileUtil::findFiles(path);
		vec.erase(std::find(vec.begin(), vec.end(), "."));
		vec.erase(std::find(vec.begin(), vec.end(), ".."));
		if (sort)
		{
			std::sort(vec.begin(), vec.end(), [](const String& x, const String& y) {return std::stoi(x) < std::stoi(y); });
		}
		return vec;
	}

	void SqliteFileManager::create(String fileName)
	{
		connection = createDB(fileName);
	}

	void SqliteFileManager::packDir(String folder)
	{
		String name = folder.substr(folder.find_last_of("/") + 1, folder.length());

		sql::connection& db = *connection;
		//auto [path_set, md5_set] = getExist();

		Tab_FDB::table_file_info table_info = {};
		Tab_FDB::table_file_data table_data = {};

		StringVector filesToRemove;

		size_t newFileCount = 0;
		StringVector zVec = findFiles(folder, true);
		for (auto& z : zVec)
		{
			StringVector yVec = findFiles(joinPath(folder, z));
			for (auto& y : yVec)
			{
				StringVector xVec = findFiles(joinPath(folder, z, y));
				for (auto& x : xVec)
				{
					String file_path = joinPath(z, y, x);
					String fileName = joinPath(folder, file_path);
					if (!existInfo(file_path))
					{
						OCPP_LOG_INFO << file_path << "\n";

						if (newFileCount == 1)
						{
							db.start_transaction();
						}
						else if (newFileCount == _commitEveryNum)
						{
							OCPP_LOG_INFO << "(" << newFileCount << ")commit_transaction\n";
							db.commit_transaction();
							newFileCount = 0;
							for (const auto& f : filesToRemove)
							{
								OCPP_LOG_INFO << f << " delete. \n";
								FileUtil::deleteFile(f);
							}
							filesToRemove.clear();
						}

						auto file_blob = FileUtil::getFileBlob(fileName);
						size_t file_size = file_blob.size();
						if (file_size == 0)
						{
							OCPP_LOG_ERROR << fileName << " incorrect. \n";
							FileUtil::deleteFile(fileName);
							continue;
						}
						String file_md5 = drogon::utils::getMd5((const char*)file_blob.data(), file_size);

						(db)(insert_into(table_info).set(
							table_info.file_md5 = file_md5,
							table_info.file_path = file_path));

						if (!existData(file_md5))
						{
							(db)(insert_into(table_data).set(
								table_data.file_size = file_size,
								table_data.file_md5 = file_md5,
								table_data.file_data = file_blob));
						}

						newFileCount++;
					}
					else if (_removeAfterInser)
					{
						//OCPP_LOG_ERROR << fileName << " delete. \n";
						//FileUtil::deleteFile(fileName);
						filesToRemove.push_back(fileName);
					}
				}
			}
		}

		if (newFileCount > 0)
		{
			OCPP_LOG_INFO << "(" << newFileCount << ")commit_transaction\n";
			db.commit_transaction();
			for (const auto& f : filesToRemove)
			{
				OCPP_LOG_INFO << f << " delete. \n";
				FileUtil::deleteFile(f);
			}
			filesToRemove.clear();
		}
	}
}
